diff --git a/src/main/AiProviders.js b/src/main/AiProviders.js
index bd3e50a..74e46a1 100644
--- a/src/main/AiProviders.js
+++ b/src/main/AiProviders.js
@@ -12,6 +12,10 @@
* - `ollama` → http://localhost:11434/v1 (OpenAI-compatible)
* - `lmstudio` → http://localhost:1234/v1 (OpenAI-compatible)
* - `openai-compatible`→ any baseUrl speaking the OpenAI chat schema
+ * - `anthropic-compatible` → any baseUrl speaking the Anthropic messages
+ * schema (LiteLLM proxies, Bedrock gateways, local Claude-compatible
+ * servers); baseUrl is required, the API key is optional because many
+ * proxies are keyless or front their own auth
*
* The module takes an injectable `fetchImpl` (defaulting to global fetch) so
* tests can stub the network without monkey-patching.
@@ -33,6 +37,7 @@ const PROVIDER_DEFAULTS = {
ollama: { baseUrl: 'http://localhost:11434/v1', defaultModel: 'llama3.1' },
lmstudio: { baseUrl: 'http://localhost:1234/v1', defaultModel: 'local-model' },
'openai-compatible': { baseUrl: '', defaultModel: '' },
+ 'anthropic-compatible': { baseUrl: '', defaultModel: 'claude-3-5-sonnet-latest' },
};
/** Provider ids that speak the OpenAI chat-completions schema. */
@@ -72,8 +77,13 @@ function resolveSettings({
);
}
- // Local providers (ollama/lmstudio) don't need a key; remote ones do.
- const needsKey = requireKey && provider !== 'ollama' && provider !== 'lmstudio';
+ // Local providers (ollama/lmstudio) and self-hosted *-compatible proxies
+ // don't need a key; branded cloud endpoints do.
+ const needsKey =
+ requireKey &&
+ provider !== 'ollama' &&
+ provider !== 'lmstudio' &&
+ provider !== 'anthropic-compatible';
if (needsKey && !apiKey) {
throw new AiProviderError(
`The "${provider}" provider needs an API key. Add one in AI Assistant settings.`,
@@ -158,13 +168,27 @@ async function callAnthropic(settings, { system, messages }, fetchImpl, timeoutM
system: system || undefined,
messages: messages.map((m) => ({ role: m.role, content: m.content })),
};
- const response = await fetchImpl(`${settings.baseUrl}/v1/messages`, {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'anthropic-version': '2023-06-01',
+ };
+ if (settings.apiKey) {
+ // Official API keys travel via x-api-key; many compatible gateways
+ // (LiteLLM proxies, Bedrock fronts) expect a Bearer token instead.
+ // Sending both is harmless for the official endpoint and maximizes
+ // proxy compatibility.
+ headers['x-api-key'] = settings.apiKey;
+ headers.Authorization = `Bearer ${settings.apiKey}`;
+ }
+ // Bases may or may not already carry the /v1 prefix — handle both so a
+ // "http://host:4000" proxy base and an "http://host:4000/v1" style base
+ // both land on a single /v1/messages path.
+ const messagesUrl = settings.baseUrl.endsWith('/v1')
+ ? `${settings.baseUrl}/messages`
+ : `${settings.baseUrl}/v1/messages`;
+ const response = await fetchImpl(messagesUrl, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'x-api-key': settings.apiKey,
- 'anthropic-version': '2023-06-01',
- },
+ headers,
body: JSON.stringify(body),
signal: controller.signal,
});
diff --git a/src/plugins/built-in/ai-assistant/index.js b/src/plugins/built-in/ai-assistant/index.js
index 74287e6..046b7f6 100644
--- a/src/plugins/built-in/ai-assistant/index.js
+++ b/src/plugins/built-in/ai-assistant/index.js
@@ -3,7 +3,10 @@
*
* Brings LLM assistance into the editor with multi-provider support:
* - OpenAI / Anthropic (cloud, API key required)
- * - Ollama / LM Studio / any OpenAI-compatible endpoint (local, no key)
+ * - Ollama / LM Studio (local, no key)
+ * - Any OpenAI-compatible or Anthropic-compatible endpoint (custom base
+ * URL; Anthropic-compatible gateways like LiteLLM proxies work with or
+ * without a key)
*
* All provider traffic is proxied through the main process
* (ai-assistant:complete / ai-assistant:status IPC) so API keys never enter
@@ -231,11 +234,18 @@ class AiAssistantPlugin extends PluginAPI {
+
-
-
+
+
Settings are stored locally in settings.json. API keys never leave this machine.
diff --git a/src/plugins/built-in/ai-assistant/manifest.json b/src/plugins/built-in/ai-assistant/manifest.json
index 589543f..89d07f9 100644
--- a/src/plugins/built-in/ai-assistant/manifest.json
+++ b/src/plugins/built-in/ai-assistant/manifest.json
@@ -19,7 +19,7 @@
"key": "provider",
"type": "string",
"default": "",
- "label": "Provider (openai|anthropic|ollama|lmstudio|openai-compatible)"
+ "label": "Provider (openai|anthropic|ollama|lmstudio|openai-compatible|anthropic-compatible)"
},
{ "key": "model", "type": "string", "default": "", "label": "Model name" },
{
diff --git a/src/plugins/built-in/collaboration/comment-store.js b/src/plugins/built-in/collaboration/comment-store.js
index a6af964..1b7aeb8 100644
--- a/src/plugins/built-in/collaboration/comment-store.js
+++ b/src/plugins/built-in/collaboration/comment-store.js
@@ -42,17 +42,20 @@ function newCommentId(now = Date.now()) {
* Load all comments for a document. Missing file/corrupt JSON → [] (a fresh
* document simply has no comments yet; a corrupt store must not crash the app).
*
+ * Async: `io.readFile`/`io.fileExists` are IPC-backed Promises in the app
+ * (plain values in tests — `await` handles both).
+ *
* @param {string} docPath Document path
* @param {object} io - { readFile: (path) => string|null, fileExists: (path) => bool }
* @param {Function} pathUtil injected path module
- * @returns {Array