diff --git a/docs/i18n/AUDIT_AND_IMPROVEMENT_PLAN.md b/docs/i18n/AUDIT_AND_IMPROVEMENT_PLAN.md
index 7f0027e9e..5ec4cb6bf 100644
--- a/docs/i18n/AUDIT_AND_IMPROVEMENT_PLAN.md
+++ b/docs/i18n/AUDIT_AND_IMPROVEMENT_PLAN.md
@@ -7,7 +7,7 @@
## 1. Current state (as audited)
-- **19 locales × 21 modules**, ~2855 keys, full **key parity** enforced by
+- **19 locales × 21 modules**, ~2856 keys, full **key parity** enforced by
`scripts/check-i18n-keys.mjs` (CI `quality` job) + bundled to `public/locales//bundle.json`.
- Runtime: custom `contexts/I18nContext.tsx` — lazy `bundle.json` fetch, `localStorage` persistence
(`worldscript-language`), English fallback chain, cached `Intl.*` helpers, `Intl.Segmenter` word
diff --git a/locales/ar/settings.json b/locales/ar/settings.json
index 883f9c151..8360e4bdb 100644
--- a/locales/ar/settings.json
+++ b/locales/ar/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "نماذج جاهزة في الذاكرة لاستدلال سريع",
"settings.ai.anthropicCorsNote": "لا يمكن استدعاء Anthropic Claude مباشرة من المتصفّح. أعدّ خادم وكيل أو استخدم تطبيق Tauri لسطح المكتب لاستدعاءات Anthropic.",
"settings.ai.anthropicHint": "يتطلب Anthropic خادم وكيل أو تطبيق Tauri لسطح المكتب لاستدعاءات API.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "فشل الاتصال",
"settings.ai.connectionSuccess": "نجح الاتصال",
"settings.ai.corsRestriction": "قيد CORS",
diff --git a/locales/de/settings.json b/locales/de/settings.json
index 69c7188f8..2c2db754a 100644
--- a/locales/de/settings.json
+++ b/locales/de/settings.json
@@ -104,8 +104,9 @@
"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. Richte einen Proxy-Server ein oder nutze die Tauri-Desktop-App für Anthropic-Aufrufe.",
- "settings.ai.anthropicHint": "Anthropic erfordert einen Proxy-Server oder die Tauri-Desktop-App für API-Aufrufe.",
+ "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.anthropicKey": "Claude API-Schlüssel",
"settings.ai.connectionFailed": "Verbindung fehlgeschlagen",
"settings.ai.connectionSuccess": "Verbindung erfolgreich",
"settings.ai.corsRestriction": "CORS-Einschränkung",
diff --git a/locales/el/settings.json b/locales/el/settings.json
index f0225af92..38f18c29f 100644
--- a/locales/el/settings.json
+++ b/locales/el/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Μοντέλα έτοιμα στη μνήμη για γρήγορη εξαγωγή συμπερασμάτων",
"settings.ai.anthropicCorsNote": "Το Anthropic Claude δεν μπορεί να κληθεί απευθείας από το πρόγραμμα περιήγησης. Ρυθμίστε έναν διακομιστή μεσολάβησης ή χρησιμοποιήστε την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις Anthropic.",
"settings.ai.anthropicHint": "Το Anthropic απαιτεί διακομιστή μεσολάβησης ή την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις API.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Η σύνδεση απέτυχε",
"settings.ai.connectionSuccess": "Επιτυχής σύνδεση",
"settings.ai.corsRestriction": "Περιορισμός CORS",
diff --git a/locales/en/settings.json b/locales/en/settings.json
index 067c26481..62d549968 100644
--- a/locales/en/settings.json
+++ b/locales/en/settings.json
@@ -104,8 +104,9 @@
"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. Set up a proxy server or use the Tauri desktop app for Anthropic calls.",
- "settings.ai.anthropicHint": "Anthropic requires a proxy server or the Tauri desktop app for API calls.",
+ "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.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Connection failed",
"settings.ai.connectionSuccess": "Connection successful",
"settings.ai.corsRestriction": "CORS Restriction",
diff --git a/locales/es/settings.json b/locales/es/settings.json
index 6edaa3006..0611d0772 100644
--- a/locales/es/settings.json
+++ b/locales/es/settings.json
@@ -104,8 +104,9 @@
"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. Configura un servidor proxy o usa la aplicación de escritorio Tauri para llamadas de Anthropic.",
- "settings.ai.anthropicHint": "Anthropic requiere un servidor proxy o la aplicación de escritorio Tauri para llamadas API.",
+ "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.anthropicKey": "Clave API de Claude",
"settings.ai.connectionFailed": "Conexión fallida",
"settings.ai.connectionSuccess": "Conexión exitosa",
"settings.ai.corsRestriction": "Restricción CORS",
diff --git a/locales/eu/settings.json b/locales/eu/settings.json
index 87f706d49..1c82d02d8 100644
--- a/locales/eu/settings.json
+++ b/locales/eu/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Memorian prest dauden ereduak inferentzia azkarrak egiteko",
"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.connectionFailed": "Konexioak huts egin du",
"settings.ai.connectionSuccess": "Konexioa arrakastatsua da",
"settings.ai.corsRestriction": "CORS Murrizketa",
diff --git a/locales/fa/settings.json b/locales/fa/settings.json
index cc1a169a0..f37baa187 100644
--- a/locales/fa/settings.json
+++ b/locales/fa/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "مدل های آماده در حافظه برای استنتاج سریع",
"settings.ai.anthropicCorsNote": "Anthropic Claude را نمی توان مستقیماً از مرورگر فراخوانی کرد. یک سرور پراکسی راه اندازی کنید یا از برنامه دسکتاپ Tauri برای تماس های Anthropic استفاده کنید.",
"settings.ai.anthropicHint": "Anthropic به یک سرور پراکسی یا برنامه دسکتاپ Tauri برای تماسهای API نیاز دارد.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "اتصال ناموفق بود",
"settings.ai.connectionSuccess": "اتصال با موفقیت انجام شد",
"settings.ai.corsRestriction": "محدودیت CORS",
diff --git a/locales/fi/settings.json b/locales/fi/settings.json
index b71ffb470..f645a1124 100644
--- a/locales/fi/settings.json
+++ b/locales/fi/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Mallit valmiina muistiin nopeaa päättelyä varten",
"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.connectionFailed": "Yhteys epäonnistui",
"settings.ai.connectionSuccess": "Yhteys onnistui",
"settings.ai.corsRestriction": "CORS-rajoitus",
diff --git a/locales/fr/settings.json b/locales/fr/settings.json
index 38cfee4d0..5299b4bcc 100644
--- a/locales/fr/settings.json
+++ b/locales/fr/settings.json
@@ -104,8 +104,9 @@
"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. Configurez un serveur proxy ou utilisez l'application de bureau Tauri pour les appels Anthropic.",
- "settings.ai.anthropicHint": "Anthropic nécessite un serveur proxy ou l'application de bureau Tauri pour les appels API.",
+ "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.anthropicKey": "Clé API Claude",
"settings.ai.connectionFailed": "Connexion échouée",
"settings.ai.connectionSuccess": "Connexion réussie",
"settings.ai.corsRestriction": "Restriction CORS",
diff --git a/locales/he/settings.json b/locales/he/settings.json
index 5a02f3053..121addbba 100644
--- a/locales/he/settings.json
+++ b/locales/he/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "מודלים מוכנים בזיכרון לאינפרנס מהיר",
"settings.ai.anthropicCorsNote": "לא ניתן לקרוא ל‑Anthropic Claude ישירות מהדפדפן. הקימו שרת פרוקסי או השתמשו באפליקציית Tauri לשולחן העבודה לקריאות Anthropic.",
"settings.ai.anthropicHint": "Anthropic דורש שרת פרוקסי או את אפליקציית Tauri לשולחן העבודה לקריאות API.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "החיבור נכשל",
"settings.ai.connectionSuccess": "החיבור הצליח",
"settings.ai.corsRestriction": "הגבלת CORS",
diff --git a/locales/hu/settings.json b/locales/hu/settings.json
index 88eddf9c4..78d2feaea 100644
--- a/locales/hu/settings.json
+++ b/locales/hu/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "A modellek készen állnak a memóriában a gyors következtetés érdekében",
"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.connectionFailed": "A csatlakozás sikertelen",
"settings.ai.connectionSuccess": "Sikeres csatlakozás",
"settings.ai.corsRestriction": "CORS korlátozás",
diff --git a/locales/is/settings.json b/locales/is/settings.json
index c5fcfcb9d..e3cd1f31a 100644
--- a/locales/is/settings.json
+++ b/locales/is/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Líkön tilbúin í minni fyrir hraða ályktanir",
"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.connectionFailed": "Tenging mistókst",
"settings.ai.connectionSuccess": "Tenging tókst",
"settings.ai.corsRestriction": "CORS takmörkun",
diff --git a/locales/it/settings.json b/locales/it/settings.json
index 0e8fb5b9c..2a9bea920 100644
--- a/locales/it/settings.json
+++ b/locales/it/settings.json
@@ -104,8 +104,9 @@
"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. Configura un server proxy o usa l'app desktop Tauri per le chiamate Anthropic.",
- "settings.ai.anthropicHint": "Anthropic richiede un server proxy o l'app desktop Tauri per le chiamate API.",
+ "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.anthropicKey": "Chiave API Claude",
"settings.ai.connectionFailed": "Connessione fallita",
"settings.ai.connectionSuccess": "Connessione riuscita",
"settings.ai.corsRestriction": "Restrizione CORS",
diff --git a/locales/ja/settings.json b/locales/ja/settings.json
index 3b475cead..d8f94cee3 100644
--- a/locales/ja/settings.json
+++ b/locales/ja/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "モデルをメモリ内に準備して高速推論を実現",
"settings.ai.anthropicCorsNote": "Anthropic Claude をブラウザから直接呼び出すことはできません。プロキシ サーバーをセットアップするか、Anthropic 呼び出し用の Tauri デスクトップ アプリを使用します。",
"settings.ai.anthropicHint": "Anthropic では、API 呼び出しのためにプロキシ サーバーまたは Tauri デスクトップ アプリが必要です。",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "接続に失敗しました",
"settings.ai.connectionSuccess": "接続成功",
"settings.ai.corsRestriction": "CORS の制限",
diff --git a/locales/ko/settings.json b/locales/ko/settings.json
index 356201fe4..d77159986 100644
--- a/locales/ko/settings.json
+++ b/locales/ko/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "빠른 추론을 위해 메모리에 준비된 모델",
"settings.ai.anthropicCorsNote": "Anthropic Claude는 브라우저에서 직접 호출할 수 없습니다. Anthropic 통화를 위해 프록시 서버를 설정하거나 Tauri 데스크톱 앱을 사용하세요.",
"settings.ai.anthropicHint": "Anthropic에는 API 호출을 위해 프록시 서버 또는 Tauri 데스크톱 앱이 필요합니다.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "연결 실패",
"settings.ai.connectionSuccess": "연결 성공",
"settings.ai.corsRestriction": "CORS 제한",
diff --git a/locales/pt/settings.json b/locales/pt/settings.json
index dd1d26cd2..8a3378801 100644
--- a/locales/pt/settings.json
+++ b/locales/pt/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Modelos prontos na memória para inferência rápida",
"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.connectionFailed": "Falha na conexão",
"settings.ai.connectionSuccess": "Conexão bem-sucedida",
"settings.ai.corsRestriction": "Restrição CORS",
diff --git a/locales/ru/settings.json b/locales/ru/settings.json
index f559ac400..def9ddf72 100644
--- a/locales/ru/settings.json
+++ b/locales/ru/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Модели, хранящиеся в памяти для быстрого вывода",
"settings.ai.anthropicCorsNote": "Anthropic Claude нельзя вызвать напрямую из браузера. Настройте прокси-сервер или используйте настольное приложение Tauri для вызовов Anthropic.",
"settings.ai.anthropicHint": "Для вызовов API Anthropic требуется прокси-сервер или настольное приложение Tauri.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Соединение не удалось",
"settings.ai.connectionSuccess": "Соединение успешно",
"settings.ai.corsRestriction": "Ограничение CORS",
diff --git a/locales/sv/settings.json b/locales/sv/settings.json
index af8d21940..8daafddde 100644
--- a/locales/sv/settings.json
+++ b/locales/sv/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Modeller redo i minnet för snabb slutledning",
"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.connectionFailed": "Anslutning misslyckades",
"settings.ai.connectionSuccess": "Anslutningen lyckades",
"settings.ai.corsRestriction": "CORS-begränsning",
diff --git a/locales/zh/settings.json b/locales/zh/settings.json
index c608047f1..e78001d3f 100644
--- a/locales/zh/settings.json
+++ b/locales/zh/settings.json
@@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "模型已在内存中准备好进行快速推理",
"settings.ai.anthropicCorsNote": "Anthropic Claude 无法直接从浏览器调用。设置代理服务器或使用 Tauri 桌面应用程序进行 Anthropic 呼叫。",
"settings.ai.anthropicHint": "Anthropic 需要代理服务器或 Tauri 桌面应用程序来进行 API 调用。",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "连接失败",
"settings.ai.connectionSuccess": "连接成功",
"settings.ai.corsRestriction": "CORS 限制",
diff --git a/public/locales/ar/bundle.json b/public/locales/ar/bundle.json
index bface19ad..a1eb08c49 100644
--- a/public/locales/ar/bundle.json
+++ b/public/locales/ar/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "نماذج جاهزة في الذاكرة لاستدلال سريع",
"settings.ai.anthropicCorsNote": "لا يمكن استدعاء Anthropic Claude مباشرة من المتصفّح. أعدّ خادم وكيل أو استخدم تطبيق Tauri لسطح المكتب لاستدعاءات Anthropic.",
"settings.ai.anthropicHint": "يتطلب Anthropic خادم وكيل أو تطبيق Tauri لسطح المكتب لاستدعاءات API.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "فشل الاتصال",
"settings.ai.connectionSuccess": "نجح الاتصال",
"settings.ai.corsRestriction": "قيد CORS",
diff --git a/public/locales/de/bundle.json b/public/locales/de/bundle.json
index f09ca0a02..718ae2755 100644
--- a/public/locales/de/bundle.json
+++ b/public/locales/de/bundle.json
@@ -1722,8 +1722,9 @@
"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. Richte einen Proxy-Server ein oder nutze die Tauri-Desktop-App für Anthropic-Aufrufe.",
- "settings.ai.anthropicHint": "Anthropic erfordert einen Proxy-Server oder die Tauri-Desktop-App für API-Aufrufe.",
+ "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.anthropicKey": "Claude API-Schlüssel",
"settings.ai.connectionFailed": "Verbindung fehlgeschlagen",
"settings.ai.connectionSuccess": "Verbindung erfolgreich",
"settings.ai.corsRestriction": "CORS-Einschränkung",
diff --git a/public/locales/el/bundle.json b/public/locales/el/bundle.json
index f7b4fc6e6..eb44ec1c3 100644
--- a/public/locales/el/bundle.json
+++ b/public/locales/el/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "Μοντέλα έτοιμα στη μνήμη για γρήγορη εξαγωγή συμπερασμάτων",
"settings.ai.anthropicCorsNote": "Το Anthropic Claude δεν μπορεί να κληθεί απευθείας από το πρόγραμμα περιήγησης. Ρυθμίστε έναν διακομιστή μεσολάβησης ή χρησιμοποιήστε την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις Anthropic.",
"settings.ai.anthropicHint": "Το Anthropic απαιτεί διακομιστή μεσολάβησης ή την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις API.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Η σύνδεση απέτυχε",
"settings.ai.connectionSuccess": "Επιτυχής σύνδεση",
"settings.ai.corsRestriction": "Περιορισμός CORS",
diff --git a/public/locales/en/bundle.json b/public/locales/en/bundle.json
index 0202fdaa3..7a5dc6fe7 100644
--- a/public/locales/en/bundle.json
+++ b/public/locales/en/bundle.json
@@ -1722,8 +1722,9 @@
"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. Set up a proxy server or use the Tauri desktop app for Anthropic calls.",
- "settings.ai.anthropicHint": "Anthropic requires a proxy server or the Tauri desktop app for API calls.",
+ "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.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Connection failed",
"settings.ai.connectionSuccess": "Connection successful",
"settings.ai.corsRestriction": "CORS Restriction",
diff --git a/public/locales/es/bundle.json b/public/locales/es/bundle.json
index 536a51313..736445000 100644
--- a/public/locales/es/bundle.json
+++ b/public/locales/es/bundle.json
@@ -1722,8 +1722,9 @@
"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. Configura un servidor proxy o usa la aplicación de escritorio Tauri para llamadas de Anthropic.",
- "settings.ai.anthropicHint": "Anthropic requiere un servidor proxy o la aplicación de escritorio Tauri para llamadas API.",
+ "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.anthropicKey": "Clave API de Claude",
"settings.ai.connectionFailed": "Conexión fallida",
"settings.ai.connectionSuccess": "Conexión exitosa",
"settings.ai.corsRestriction": "Restricción CORS",
diff --git a/public/locales/eu/bundle.json b/public/locales/eu/bundle.json
index 36cc87f40..1be517aac 100644
--- a/public/locales/eu/bundle.json
+++ b/public/locales/eu/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "Memorian prest dauden ereduak inferentzia azkarrak egiteko",
"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.connectionFailed": "Konexioak huts egin du",
"settings.ai.connectionSuccess": "Konexioa arrakastatsua da",
"settings.ai.corsRestriction": "CORS Murrizketa",
diff --git a/public/locales/fa/bundle.json b/public/locales/fa/bundle.json
index f45a93bea..547aa5b4b 100644
--- a/public/locales/fa/bundle.json
+++ b/public/locales/fa/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "مدل های آماده در حافظه برای استنتاج سریع",
"settings.ai.anthropicCorsNote": "Anthropic Claude را نمی توان مستقیماً از مرورگر فراخوانی کرد. یک سرور پراکسی راه اندازی کنید یا از برنامه دسکتاپ Tauri برای تماس های Anthropic استفاده کنید.",
"settings.ai.anthropicHint": "Anthropic به یک سرور پراکسی یا برنامه دسکتاپ Tauri برای تماسهای API نیاز دارد.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "اتصال ناموفق بود",
"settings.ai.connectionSuccess": "اتصال با موفقیت انجام شد",
"settings.ai.corsRestriction": "محدودیت CORS",
diff --git a/public/locales/fi/bundle.json b/public/locales/fi/bundle.json
index f47e40256..6a8469e35 100644
--- a/public/locales/fi/bundle.json
+++ b/public/locales/fi/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "Mallit valmiina muistiin nopeaa päättelyä varten",
"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.connectionFailed": "Yhteys epäonnistui",
"settings.ai.connectionSuccess": "Yhteys onnistui",
"settings.ai.corsRestriction": "CORS-rajoitus",
diff --git a/public/locales/fr/bundle.json b/public/locales/fr/bundle.json
index 5bc2d9cdd..4dd238b0a 100644
--- a/public/locales/fr/bundle.json
+++ b/public/locales/fr/bundle.json
@@ -1722,8 +1722,9 @@
"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. Configurez un serveur proxy ou utilisez l'application de bureau Tauri pour les appels Anthropic.",
- "settings.ai.anthropicHint": "Anthropic nécessite un serveur proxy ou l'application de bureau Tauri pour les appels API.",
+ "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.anthropicKey": "Clé API Claude",
"settings.ai.connectionFailed": "Connexion échouée",
"settings.ai.connectionSuccess": "Connexion réussie",
"settings.ai.corsRestriction": "Restriction CORS",
diff --git a/public/locales/he/bundle.json b/public/locales/he/bundle.json
index c9e8f28f3..336c9a680 100644
--- a/public/locales/he/bundle.json
+++ b/public/locales/he/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "מודלים מוכנים בזיכרון לאינפרנס מהיר",
"settings.ai.anthropicCorsNote": "לא ניתן לקרוא ל‑Anthropic Claude ישירות מהדפדפן. הקימו שרת פרוקסי או השתמשו באפליקציית Tauri לשולחן העבודה לקריאות Anthropic.",
"settings.ai.anthropicHint": "Anthropic דורש שרת פרוקסי או את אפליקציית Tauri לשולחן העבודה לקריאות API.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "החיבור נכשל",
"settings.ai.connectionSuccess": "החיבור הצליח",
"settings.ai.corsRestriction": "הגבלת CORS",
diff --git a/public/locales/hu/bundle.json b/public/locales/hu/bundle.json
index 434c749ac..e14393a34 100644
--- a/public/locales/hu/bundle.json
+++ b/public/locales/hu/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "A modellek készen állnak a memóriában a gyors következtetés érdekében",
"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.connectionFailed": "A csatlakozás sikertelen",
"settings.ai.connectionSuccess": "Sikeres csatlakozás",
"settings.ai.corsRestriction": "CORS korlátozás",
diff --git a/public/locales/is/bundle.json b/public/locales/is/bundle.json
index 44dec7b03..f44a44cf4 100644
--- a/public/locales/is/bundle.json
+++ b/public/locales/is/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "Líkön tilbúin í minni fyrir hraða ályktanir",
"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.connectionFailed": "Tenging mistókst",
"settings.ai.connectionSuccess": "Tenging tókst",
"settings.ai.corsRestriction": "CORS takmörkun",
diff --git a/public/locales/it/bundle.json b/public/locales/it/bundle.json
index 1736eaba1..0f30552b1 100644
--- a/public/locales/it/bundle.json
+++ b/public/locales/it/bundle.json
@@ -1722,8 +1722,9 @@
"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. Configura un server proxy o usa l'app desktop Tauri per le chiamate Anthropic.",
- "settings.ai.anthropicHint": "Anthropic richiede un server proxy o l'app desktop Tauri per le chiamate API.",
+ "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.anthropicKey": "Chiave API Claude",
"settings.ai.connectionFailed": "Connessione fallita",
"settings.ai.connectionSuccess": "Connessione riuscita",
"settings.ai.corsRestriction": "Restrizione CORS",
diff --git a/public/locales/ja/bundle.json b/public/locales/ja/bundle.json
index e96ef378a..777ee8480 100644
--- a/public/locales/ja/bundle.json
+++ b/public/locales/ja/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "モデルをメモリ内に準備して高速推論を実現",
"settings.ai.anthropicCorsNote": "Anthropic Claude をブラウザから直接呼び出すことはできません。プロキシ サーバーをセットアップするか、Anthropic 呼び出し用の Tauri デスクトップ アプリを使用します。",
"settings.ai.anthropicHint": "Anthropic では、API 呼び出しのためにプロキシ サーバーまたは Tauri デスクトップ アプリが必要です。",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "接続に失敗しました",
"settings.ai.connectionSuccess": "接続成功",
"settings.ai.corsRestriction": "CORS の制限",
diff --git a/public/locales/ko/bundle.json b/public/locales/ko/bundle.json
index 885a47306..44987fc6c 100644
--- a/public/locales/ko/bundle.json
+++ b/public/locales/ko/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "빠른 추론을 위해 메모리에 준비된 모델",
"settings.ai.anthropicCorsNote": "Anthropic Claude는 브라우저에서 직접 호출할 수 없습니다. Anthropic 통화를 위해 프록시 서버를 설정하거나 Tauri 데스크톱 앱을 사용하세요.",
"settings.ai.anthropicHint": "Anthropic에는 API 호출을 위해 프록시 서버 또는 Tauri 데스크톱 앱이 필요합니다.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "연결 실패",
"settings.ai.connectionSuccess": "연결 성공",
"settings.ai.corsRestriction": "CORS 제한",
diff --git a/public/locales/pt/bundle.json b/public/locales/pt/bundle.json
index 7741249f4..a53336ee5 100644
--- a/public/locales/pt/bundle.json
+++ b/public/locales/pt/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "Modelos prontos na memória para inferência rápida",
"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.connectionFailed": "Falha na conexão",
"settings.ai.connectionSuccess": "Conexão bem-sucedida",
"settings.ai.corsRestriction": "Restrição CORS",
diff --git a/public/locales/ru/bundle.json b/public/locales/ru/bundle.json
index 1ff99808a..88afeb0f8 100644
--- a/public/locales/ru/bundle.json
+++ b/public/locales/ru/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "Модели, хранящиеся в памяти для быстрого вывода",
"settings.ai.anthropicCorsNote": "Anthropic Claude нельзя вызвать напрямую из браузера. Настройте прокси-сервер или используйте настольное приложение Tauri для вызовов Anthropic.",
"settings.ai.anthropicHint": "Для вызовов API Anthropic требуется прокси-сервер или настольное приложение Tauri.",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Соединение не удалось",
"settings.ai.connectionSuccess": "Соединение успешно",
"settings.ai.corsRestriction": "Ограничение CORS",
diff --git a/public/locales/sv/bundle.json b/public/locales/sv/bundle.json
index 2a3100130..fb52f63e1 100644
--- a/public/locales/sv/bundle.json
+++ b/public/locales/sv/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "Modeller redo i minnet för snabb slutledning",
"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.connectionFailed": "Anslutning misslyckades",
"settings.ai.connectionSuccess": "Anslutningen lyckades",
"settings.ai.corsRestriction": "CORS-begränsning",
diff --git a/public/locales/zh/bundle.json b/public/locales/zh/bundle.json
index 94d50873d..933af99eb 100644
--- a/public/locales/zh/bundle.json
+++ b/public/locales/zh/bundle.json
@@ -1724,6 +1724,7 @@
"settings.ai.adaptive.warmedModelsHint": "模型已在内存中准备好进行快速推理",
"settings.ai.anthropicCorsNote": "Anthropic Claude 无法直接从浏览器调用。设置代理服务器或使用 Tauri 桌面应用程序进行 Anthropic 呼叫。",
"settings.ai.anthropicHint": "Anthropic 需要代理服务器或 Tauri 桌面应用程序来进行 API 调用。",
+ "settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "连接失败",
"settings.ai.connectionSuccess": "连接成功",
"settings.ai.corsRestriction": "CORS 限制",
diff --git a/services/aiProviderService.ts b/services/aiProviderService.ts
index a4da3b001..b0ea2565a 100644
--- a/services/aiProviderService.ts
+++ b/services/aiProviderService.ts
@@ -241,15 +241,41 @@ 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).
async function streamAnthropic(
- _prompt: string,
- _opts: AIRequestOptions,
- _callbacks: AIStreamCallbacks,
+ prompt: string,
+ opts: AIRequestOptions,
+ callbacks: AIStreamCallbacks,
): Promise {
- throw new Error(
- 'Claude/Anthropic: Direct browser requests are blocked by Anthropic (CORS). ' +
- 'Please use a backend proxy or switch to Gemini/OpenAI/Ollama.',
- );
+ if (!isTauriRuntime()) {
+ throw new Error(
+ 'Claude/Anthropic: Direct browser requests are blocked by Anthropic (CORS). ' +
+ 'Please use a backend proxy or switch to Gemini/OpenAI/Ollama.',
+ );
+ }
+ 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', {
+ 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,
+ });
+ 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 }> };
+ const text = json.content?.find((c) => c.type === 'text')?.text ?? '';
+ if (text) callbacks.onChunk(text);
+ callbacks.onDone?.();
}
async function streamGrok(
@@ -373,10 +399,17 @@ async function generateTextSingleProvider(
});
return providerTextSchema.parse({ text: result }).text;
}
- case 'anthropic':
- throw new Error(
- 'Claude/Anthropic is currently not available in the browser. Please use Gemini, OpenAI or Ollama.',
- );
+ 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.
+ let result = '';
+ await streamAnthropic(prompt, o, {
+ onChunk: (text) => {
+ result += text;
+ },
+ });
+ return providerTextSchema.parse({ text: result }).text;
+ }
case 'grok': {
let result = '';
await streamGrok(prompt, o, {
@@ -818,12 +851,50 @@ export async function testAIConnection(
}
return testOllamaConnection(opts.ollamaBaseUrl);
}
- case 'anthropic':
- return {
- ok: false,
- error: 'Claude requires a backend proxy (CORS restriction)',
- kind: 'backendProxyRequired',
- };
+ 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()) {
+ return {
+ ok: false,
+ error: 'Claude requires the desktop app or a backend proxy (browser CORS restriction)',
+ kind: 'backendProxyRequired',
+ };
+ }
+ const apiKey = await storageService.getApiKey('anthropic');
+ if (!apiKey) {
+ return {
+ ok: false,
+ error: 'Kein Claude API Key gesetzt',
+ kind: 'noApiKey',
+ params: { provider: 'Claude' },
+ };
+ }
+ // QNBS-v3: Anthropic has no public /v1/models endpoint — a minimal (max_tokens: 1) real
+ // request is the practical connectivity check, mirroring the pattern used for Grok.
+ 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' }],
+ }),
+ });
+ if (!res.ok) {
+ return {
+ ok: false,
+ error: `HTTP ${res.status}`,
+ kind: 'httpError',
+ params: { status: res.status },
+ };
+ }
+ return { ok: true };
+ }
case 'grok': {
const apiKey = await storageService.getApiKey('grok');
if (!apiKey) {
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index a9ee0d9f9..c57c35bda 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -24,7 +24,7 @@
}
],
"security": {
- "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src 'self' https://generativelanguage.googleapis.com https://api.openai.com https://api.x.ai http://localhost:11434 http://127.0.0.1:11434 http://localhost:1234 http://127.0.0.1:1234 http://localhost:8000 http://127.0.0.1:8000 http://localhost:8010 http://127.0.0.1:8010 https://openrouter.ai https://api.openrouter.ai https://api.groq.com https://huggingface.co https://us.aws.cdn.hf.co wss://y-webrtc-signaling.fly.dev wss://signaling.yjs.dev; img-src 'self' data: blob:; media-src 'self'; worker-src 'self' blob:; frame-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"
+ "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src 'self' https://generativelanguage.googleapis.com https://api.openai.com https://api.x.ai https://api.anthropic.com http://localhost:11434 http://127.0.0.1:11434 http://localhost:1234 http://127.0.0.1:1234 http://localhost:8000 http://127.0.0.1:8000 http://localhost:8010 http://127.0.0.1:8010 https://openrouter.ai https://api.openrouter.ai https://api.groq.com https://huggingface.co https://us.aws.cdn.hf.co wss://y-webrtc-signaling.fly.dev wss://signaling.yjs.dev; img-src 'self' data: blob:; media-src 'self'; worker-src 'self' blob:; frame-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"
}
},
"bundle": {
diff --git a/tests/unit/aiProviderService.test.ts b/tests/unit/aiProviderService.test.ts
index eac9a653a..6975e2896 100644
--- a/tests/unit/aiProviderService.test.ts
+++ b/tests/unit/aiProviderService.test.ts
@@ -32,6 +32,15 @@ vi.mock('../../services/ollamaService', () => ({
testOllamaConnection: vi.fn().mockResolvedValue({ ok: true }),
}));
+// QNBS-v3 (ADR-0016 Track A): localServerFetch's own Tauri-vs-web routing is already fully
+// covered by localServerHttp.test.ts — mock only its native dependency (plugin-http) here,
+// exactly like that file does, so the real localServerFetch (used by both the new Anthropic
+// path and the pre-existing local-server scan) keeps working for every other test in this file.
+const mockPluginHttpFetch = vi.fn();
+vi.mock('@tauri-apps/plugin-http', () => ({
+ fetch: (...args: unknown[]) => mockPluginHttpFetch(...args),
+}));
+
import {
generateImage,
generateJson,
@@ -322,6 +331,90 @@ 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)', () => {
+ const originalFetch = globalThis.fetch;
+
+ afterEach(() => {
+ delete (window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
+ globalThis.fetch = originalFetch;
+ });
+
+ it('testAIConnection: returns noApiKey when no Claude key is stored under Tauri', async () => {
+ (window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ = {};
+ vi.mocked(storageService.getApiKey).mockResolvedValueOnce(null);
+ const result = await testAIConnection('anthropic', {});
+ expect(result.ok).toBe(false);
+ expect(result.kind).toBe('noApiKey');
+ });
+
+ it('testAIConnection: returns ok:true when the native call succeeds under Tauri', async () => {
+ (window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ = {};
+ vi.mocked(storageService.getApiKey).mockResolvedValueOnce('anthropic-key');
+ mockPluginHttpFetch.mockResolvedValueOnce(new Response('{}', { status: 200 }));
+ const result = await testAIConnection('anthropic', {});
+ expect(result.ok).toBe(true);
+ expect(mockPluginHttpFetch).toHaveBeenCalledWith(
+ 'https://api.anthropic.com/v1/messages',
+ expect.objectContaining({
+ method: 'POST',
+ headers: expect.objectContaining({ 'x-api-key': 'anthropic-key' }),
+ }),
+ );
+ });
+
+ it('testAIConnection: returns httpError when the native call fails under Tauri', async () => {
+ (window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ = {};
+ vi.mocked(storageService.getApiKey).mockResolvedValueOnce('anthropic-key');
+ mockPluginHttpFetch.mockResolvedValueOnce(new Response('{}', { status: 401 }));
+ const result = await testAIConnection('anthropic', {});
+ expect(result.ok).toBe(false);
+ expect(result.kind).toBe('httpError');
+ expect(result.params).toEqual({ status: 401 });
+ });
+
+ it('testAIConnection: still returns backendProxyRequired outside Tauri (web unchanged)', async () => {
+ const result = await testAIConnection('anthropic', {});
+ expect(result.ok).toBe(false);
+ expect(result.kind).toBe('backendProxyRequired');
+ expect(mockPluginHttpFetch).not.toHaveBeenCalled();
+ });
+
+ it('streamText: delivers a real Claude response via onChunk under Tauri instead of throwing', async () => {
+ (window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ = {};
+ vi.mocked(storageService.getApiKey).mockResolvedValueOnce('anthropic-key');
+ mockPluginHttpFetch.mockResolvedValueOnce(
+ new Response(JSON.stringify({ content: [{ type: 'text', text: 'Hello from Claude' }] }), {
+ status: 200,
+ }),
+ );
+ const onChunk = vi.fn();
+ await streamText(
+ 'hello',
+ 'Balanced',
+ { provider: 'anthropic', model: 'claude-haiku-4-5' },
+ { onChunk },
+ );
+ expect(onChunk).toHaveBeenCalledWith('Hello from Claude');
+ });
+
+ it('streamText: still throws the CORS message outside Tauri (web unchanged)', async () => {
+ await expect(
+ streamText(
+ 'hello',
+ 'Balanced',
+ { provider: 'anthropic', model: 'claude-haiku-4-5' },
+ {
+ onChunk: vi.fn(),
+ },
+ ),
+ ).rejects.toThrow('CORS');
+ expect(mockPluginHttpFetch).not.toHaveBeenCalled();
+ });
+});
+
// ─── streamText (OpenAI signal + Ollama→Gemini fallback) ────────────────────
describe('streamText OpenAI', () => {
diff --git a/tests/unit/settings/AiProviderCard.test.tsx b/tests/unit/settings/AiProviderCard.test.tsx
index 9244fe93d..ceb2da350 100644
--- a/tests/unit/settings/AiProviderCard.test.tsx
+++ b/tests/unit/settings/AiProviderCard.test.tsx
@@ -382,3 +382,63 @@ 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)', () => {
+ const anthropicAdvancedAi = {
+ ...mockAdvancedAi,
+ provider: 'anthropic' as const,
+ model: 'claude-haiku-4-5' as const,
+ };
+
+ afterEach(() => {
+ setDesktopRuntime(false);
+ });
+
+ it('web: shows the CORS warning block, no key input', () => {
+ setDesktopRuntime(false);
+ render(
+ ,
+ );
+ expect(screen.getByText('settings.ai.corsRestriction')).toBeTruthy();
+ expect(screen.queryByLabelText('settings.ai.anthropicKey')).toBeNull();
+ });
+
+ it('desktop: shows the real key input and model selector, no warning block', () => {
+ 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();
+ });
+
+ it('desktop: saves the entered key via storageService.saveApiKey("anthropic", ...)', async () => {
+ setDesktopRuntime(true);
+ 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');
+ });
+ });
+});
From 744401b91914883e88de5ac0817ce1dc33d1c503 Mon Sep 17 00:00:00 2001
From: qnbs <155236708+qnbs@users.noreply.github.com>
Date: Thu, 30 Jul 2026 22:18:38 +0200
Subject: [PATCH 6/7] docs: address CodeRabbit quick-win findings on PR #299
Add missing single-line QNBS-v3 rationale comments (Grok key state,
key-persistence handler, the openaiCompatible-reuse decision in
worldScriptCompletionFetch.ts, two test fixtures/blocks) and collapse
four multi-line QNBS-v3 comments to the required single line.
Not addressed here (documented, not fixed): the ADR-0016 wording
describing Claude desktop as immediately usable is accurate once the
full stacked sequence merges (Track A is PR #300, already stacked next)
and will read correctly at that point without further edits; the
locale-translation findings for settings.ai.grokKey reflect this repo's
established i18n tiering (5 production locales get hand translations,
14 beta locales intentionally carry an EN fallback per CLAUDE.md's own
i18n section), not a defect; the vi.mock hoisting concern in
listenerMiddleware.test.ts is a false positive verified against the
actual passing suite -- ProjectDocBinding is consumed via a dynamic
await import() inside test bodies, not a static top-level import, so
by the time any mock factory runs the module has already evaluated
top-to-bottom once.
---
components/settings/AiProviderCard.tsx | 2 ++
services/ai/worldScriptCompletionFetch.ts | 2 ++
tests/unit/listenerMiddleware.test.ts | 7 ++-----
tests/unit/providerFactory.test.ts | 3 +--
tests/unit/settings/AiProviderCard.test.tsx | 5 +++--
5 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/components/settings/AiProviderCard.tsx b/components/settings/AiProviderCard.tsx
index caf89c491..57707f66e 100644
--- a/components/settings/AiProviderCard.tsx
+++ b/components/settings/AiProviderCard.tsx
@@ -46,6 +46,7 @@ export const AiProviderCard: FC = ({
// imply a verified connection next to the "desktop app required" banner.
const ollamaUntestable = provider === 'ollama' && !isDesktop;
const [openaiKey, setOpenaiKey] = useState('');
+ // QNBS-v3: Grok's own key input state, mirroring OpenAI's pattern above.
const [grokKey, setGrokKey] = useState('');
const [isSavingGrokKey, setIsSavingGrokKey] = useState(false);
const [ollamaModels, setOllamaModels] = useState([]);
@@ -74,6 +75,7 @@ export const AiProviderCard: FC = ({
.catch(() => {});
}, []);
+ // QNBS-v3: save/clear via storageService, matching every other provider's key persistence.
const handleSaveGrokKey = useCallback(async () => {
setIsSavingGrokKey(true);
try {
diff --git a/services/ai/worldScriptCompletionFetch.ts b/services/ai/worldScriptCompletionFetch.ts
index b547cb6f8..55734a4d7 100644
--- a/services/ai/worldScriptCompletionFetch.ts
+++ b/services/ai/worldScriptCompletionFetch.ts
@@ -81,6 +81,8 @@ async function resolveModelConfig(
status: 422,
};
}
+ // QNBS-v3: xAI's API is OpenAI-Chat-Completions-compatible, so Grok reuses the
+ // 'openaiCompatible' shape with a fixed baseURL instead of needing its own branch type.
if (kind === 'grok') {
const apiKey = await storageService.getApiKey('grok');
if (!apiKey) {
diff --git a/tests/unit/listenerMiddleware.test.ts b/tests/unit/listenerMiddleware.test.ts
index 867f08bba..edf3e7d51 100644
--- a/tests/unit/listenerMiddleware.test.ts
+++ b/tests/unit/listenerMiddleware.test.ts
@@ -59,8 +59,7 @@ vi.mock('../../services/storageBackend', () => ({
saveEnvelopeFromProjectData: (data: unknown) => mockSaveEnvelope(data),
}));
-// QNBS-v3: stub the real Yjs doc/binding/persistence chain so the local-first sync test can assert
-// the exact payload selectProjectData hands to the binding, instead of depending on real Yjs behavior.
+// QNBS-v3: stub the real Yjs doc/binding/persistence chain so the test asserts selectProjectData's exact payload, not real Yjs behavior.
const mockSyncFromProject = vi.fn();
const mockVerify = vi.fn().mockReturnValue({ ok: true, mismatches: [] });
const mockReproject = vi.fn();
@@ -68,9 +67,7 @@ let lastBindingCtorArg: unknown;
vi.mock('../../services/localFirst/projectDoc', () => ({
createBlankProjectDoc: vi.fn(() => ({})),
}));
-// QNBS-v3: a class, not a function expression — the real code calls `new ProjectDocBinding(...)`,
-// and Biome's useArrowFunction rule would otherwise rewrite a function expression into a
-// non-constructible arrow function here.
+// QNBS-v3: a class, not a function expression — the real code calls `new ProjectDocBinding(...)`, and Biome's useArrowFunction rule would otherwise rewrite a function expression into a non-constructible arrow function.
class MockProjectDocBinding {
syncFromProject = mockSyncFromProject;
verify = mockVerify;
diff --git a/tests/unit/providerFactory.test.ts b/tests/unit/providerFactory.test.ts
index 3d3f82801..e54c08c27 100644
--- a/tests/unit/providerFactory.test.ts
+++ b/tests/unit/providerFactory.test.ts
@@ -64,8 +64,7 @@ describe('providerToKind', () => {
});
it('maps grok to its own grok kind (not folded into openaiCompatible)', () => {
- // QNBS-v3: grok needs a fixed baseURL + a real stored key, distinct from the Ollama
- // default that also maps to 'openaiCompatible' — see providerFactory.ts's own comment.
+ // QNBS-v3: distinct from Ollama's 'openaiCompatible' default — grok needs a fixed baseURL + a real key.
expect(providerToKind('grok')).toBe('grok');
});
});
diff --git a/tests/unit/settings/AiProviderCard.test.tsx b/tests/unit/settings/AiProviderCard.test.tsx
index 9244fe93d..8a79f5d09 100644
--- a/tests/unit/settings/AiProviderCard.test.tsx
+++ b/tests/unit/settings/AiProviderCard.test.tsx
@@ -64,6 +64,7 @@ const mockOnAdvancedAiPatch = vi.fn();
const mockOnProviderChange = vi.fn();
const ollamaAdvancedAi = { ...mockAdvancedAi, provider: 'ollama' as const };
+// QNBS-v3: fixture for the grok-provider describe block below.
const grokAdvancedAi = { ...mockAdvancedAi, provider: 'grok' as const, model: 'grok-3' as const };
function setDesktopRuntime(enabled: boolean): void {
@@ -343,6 +344,7 @@ describe('AiProviderCard — ollama provider (#266)', () => {
});
});
+// QNBS-v3: covers the new grok key-input/model-selector UI added in Phase 1 (ADR-0016).
describe('AiProviderCard — grok provider', () => {
afterEach(() => {
vi.clearAllMocks();
@@ -357,8 +359,7 @@ describe('AiProviderCard — grok provider', () => {
/>,
);
expect(screen.getByLabelText('settings.ai.grokKey')).toBeTruthy();
- // QNBS-v3: the Select renders only the currently-selected option's label when closed —
- // "Grok 3 Mini" only appears once the dropdown is opened, not asserted here.
+ // QNBS-v3: the Select renders only the currently-selected option's label when closed, so "Grok 3 Mini" isn't asserted here.
await waitFor(() => {
expect(screen.getByText('Grok 3')).toBeTruthy();
});
From 07c5e6af45d8f74b112de16c84c7b38840a3499f Mon Sep 17 00:00:00 2001
From: qnbs <155236708+qnbs@users.noreply.github.com>
Date: Thu, 30 Jul 2026 23:07:38 +0200
Subject: [PATCH 7/7] fix(ai): concatenate all Claude text blocks, bound the
connectivity ping
Two Major CodeRabbit findings on PR #300, both real:
- streamAnthropic() used content?.find(c => c.type === 'text') -- Claude
can return multiple content blocks (e.g. a 'thinking' block plus
several 'text' blocks on extended-thinking models), and find() silently
dropped everything after the first match. Now filters and concatenates
every text block in order.
- testAIConnection's anthropic ping had no timeout, unlike every sibling
connectivity check (testOllamaConnection: timeoutMs 5000; openai/grok/
gemini: AbortSignal.timeout(8000)) -- a stalled native HTTP call could
hang the Settings test spinner indefinitely. Added timeoutMs: 8000.
Deliberately NOT applied to streamAnthropic's own outbound call (the
second location CodeRabbit's finding named): real generation calls
across this file intentionally stay unbounded by a fixed timeout,
using only the caller's own AbortSignal for cancellation -- Ollama's
real generation call (streamOllama) follows the same pattern, only its
connectivity check gets timeoutMs. Special-casing Anthropic's
generation call would be the inconsistent choice, not the fix.
---
services/aiProviderService.ts | 12 +++++++++++-
tests/unit/aiProviderService.test.ts | 28 ++++++++++++++++++++++++++++
2 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/services/aiProviderService.ts b/services/aiProviderService.ts
index b0ea2565a..a5f8f08f1 100644
--- a/services/aiProviderService.ts
+++ b/services/aiProviderService.ts
@@ -273,7 +273,13 @@ async function streamAnthropic(
});
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 }> };
- const text = json.content?.find((c) => c.type === 'text')?.text ?? '';
+ // 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?.();
}
@@ -872,6 +878,9 @@ export async function testAIConnection(
}
// QNBS-v3: Anthropic has no public /v1/models endpoint — a minimal (max_tokens: 1) real
// 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: {
@@ -884,6 +893,7 @@ export async function testAIConnection(
max_tokens: 1,
messages: [{ role: 'user', content: 'ping' }],
}),
+ timeoutMs: 8000,
});
if (!res.ok) {
return {
diff --git a/tests/unit/aiProviderService.test.ts b/tests/unit/aiProviderService.test.ts
index 6975e2896..0b828987f 100644
--- a/tests/unit/aiProviderService.test.ts
+++ b/tests/unit/aiProviderService.test.ts
@@ -361,6 +361,9 @@ describe('Anthropic — desktop branch (ADR-0016 Track A)', () => {
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ 'x-api-key': 'anthropic-key' }),
+ // QNBS-v3 (CodeRabbit): bounded like every sibling connectivity check — a real AbortSignal,
+ // not the unbounded `null` the pre-fix call passed.
+ signal: expect.any(AbortSignal),
}),
);
});
@@ -400,6 +403,31 @@ describe('Anthropic — desktop branch (ADR-0016 Track A)', () => {
expect(onChunk).toHaveBeenCalledWith('Hello from Claude');
});
+ 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');
+ mockPluginHttpFetch.mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ content: [
+ { type: 'thinking', text: 'internal reasoning, not user-facing' },
+ { type: 'text', text: 'Part one. ' },
+ { type: 'text', text: 'Part two.' },
+ ],
+ }),
+ { status: 200 },
+ ),
+ );
+ const onChunk = vi.fn();
+ await streamText(
+ 'hello',
+ 'Balanced',
+ { provider: 'anthropic', model: 'claude-haiku-4-5' },
+ { onChunk },
+ );
+ expect(onChunk).toHaveBeenCalledWith('Part one. Part two.');
+ });
+
it('streamText: still throws the CORS message outside Tauri (web unchanged)', async () => {
await expect(
streamText(