diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md index 0ea3624df..713a04ad7 100644 --- a/docs/01-getting-started.md +++ b/docs/01-getting-started.md @@ -253,6 +253,8 @@ OpenRouter is a universal AI API gateway — one account gives you access to eve Why OpenRouter instead of OpenAI directly? One account, one key, one billing relationship — and it future-proofs you for Claude, Gemini, or any other model later. +Atlas Cloud is also supported as an OpenAI-compatible backend for the core MCP server. To use it instead of OpenRouter, set `OPEN_BRAIN_AI_PROVIDER=atlascloud` and `ATLASCLOUD_API_KEY`, and keep the embedding dimension at 1536 unless you also update the database vector column. + 1. Go to [openrouter.ai](https://openrouter.ai) and sign up 2. Go to [openrouter.ai/keys](https://openrouter.ai/keys) 3. Click **Create Key**, name it `open-brain` @@ -412,6 +414,12 @@ Set your OpenRouter key from Step 4: supabase secrets set OPENROUTER_API_KEY=your-openrouter-key-here ``` +Or, to use Atlas Cloud instead: + +```bash +supabase secrets set OPEN_BRAIN_AI_PROVIDER=atlascloud ATLASCLOUD_API_KEY=your-atlascloud-key-here +``` + > [!NOTE] > `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are automatically available inside Edge Functions — you don't need to set them. > @@ -594,6 +602,12 @@ Set your OpenRouter key from Step 4: supabase secrets set OPENROUTER_API_KEY=your-openrouter-key-here ``` +Or, to use Atlas Cloud instead: + +```powershell +supabase secrets set OPEN_BRAIN_AI_PROVIDER=atlascloud ATLASCLOUD_API_KEY=your-atlascloud-key-here +``` + > [!NOTE] > `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are automatically available inside Edge Functions — you don't need to set them. > @@ -912,7 +926,7 @@ The embedding is what makes retrieval powerful. "Sarah's thinking about leaving" ### Swapping Models Later -Because you're using OpenRouter, you can swap models by editing the model strings in the Edge Function code and redeploying. Browse available models at [openrouter.ai/models](https://openrouter.ai/models). Just make sure embedding dimensions match (1536 for the current setup). +Because the server uses OpenAI-compatible endpoints, you can swap models by setting `OPEN_BRAIN_CHAT_MODEL` or `OPEN_BRAIN_EMBEDDING_MODEL` before redeploying. Browse OpenRouter models at [openrouter.ai/models](https://openrouter.ai/models), or use Atlas Cloud model IDs such as `qwen/qwen3.5-flash` for chat. Just make sure embedding dimensions match (1536 for the current setup). diff --git a/docs/open-brain-assistant-gpt-context.md b/docs/open-brain-assistant-gpt-context.md index f8b4491a3..4057d6dad 100644 --- a/docs/open-brain-assistant-gpt-context.md +++ b/docs/open-brain-assistant-gpt-context.md @@ -88,15 +88,16 @@ For Claude Desktop and ChatGPT, the `?key=` URL is usually the simplest path. Fo Open Brain setup requires: - Supabase project. -- OpenRouter account and API key. +- OpenRouter account and API key, or Atlas Cloud configured as the OpenAI-compatible backend. - Supabase CLI. - A generated MCP access key. - pgvector enabled in Supabase. -- `OPENROUTER_API_KEY` and `MCP_ACCESS_KEY` set as Supabase Edge Function secrets. +- `OPENROUTER_API_KEY` and `MCP_ACCESS_KEY` set as Supabase Edge Function secrets. For Atlas Cloud, use `OPEN_BRAIN_AI_PROVIDER=atlascloud` plus `ATLASCLOUD_API_KEY` instead of `OPENROUTER_API_KEY`. Important details: - `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are automatically available inside Supabase Edge Functions. +- OpenRouter remains the default AI backend. Atlas Cloud is selected explicitly with `OPEN_BRAIN_AI_PROVIDER=atlascloud`, or automatically when `ATLASCLOUD_API_KEY` is set and `OPENROUTER_API_KEY` is absent. - Newer Supabase projects may not grant service role table permissions by default. The user must run the documented `GRANT` SQL for the `thoughts` table. - The base embedding dimension is 1536. If users change embedding models, dimensions must still match the vector column and search function. - The `upsert_thought` function deduplicates by normalized content fingerprint and merges metadata on duplicate capture. diff --git a/server/index.ts b/server/index.ts index d3265f958..a9cb7e3e3 100644 --- a/server/index.ts +++ b/server/index.ts @@ -8,10 +8,75 @@ import { createClient } from "@supabase/supabase-js"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; -const OPENROUTER_API_KEY = Deno.env.get("OPENROUTER_API_KEY")!; const MCP_ACCESS_KEY = Deno.env.get("MCP_ACCESS_KEY")!; const OPENROUTER_BASE = "https://openrouter.ai/api/v1"; +const ATLASCLOUD_BASE = "https://api.atlascloud.ai/v1"; +const DEFAULT_OPENROUTER_EMBEDDING_MODEL = "openai/text-embedding-3-small"; +const DEFAULT_OPENROUTER_CHAT_MODEL = "openai/gpt-4o-mini"; +const DEFAULT_ATLASCLOUD_EMBEDDING_MODEL = "text-embedding-3-small"; +const DEFAULT_ATLASCLOUD_CHAT_MODEL = "qwen/qwen3.5-flash"; + +const OPENROUTER_API_KEY = Deno.env.get("OPENROUTER_API_KEY"); +const ATLASCLOUD_API_KEY = + Deno.env.get("ATLASCLOUD_API_KEY") || Deno.env.get("ATLAS_CLOUD_API_KEY"); + +type AiProvider = { + name: "OpenRouter" | "Atlas Cloud"; + baseUrl: string; + apiKey: string; + embeddingModel: string; + chatModel: string; +}; + +function trimTrailingSlash(url: string): string { + return url.replace(/\/+$/, ""); +} + +function buildAiProviderConfig(): AiProvider { + const requestedProvider = Deno.env.get("OPEN_BRAIN_AI_PROVIDER")?.toLowerCase(); + const useAtlasCloud = + requestedProvider === "atlascloud" || requestedProvider === "atlas_cloud" || + (!OPENROUTER_API_KEY && !!ATLASCLOUD_API_KEY); + + if (useAtlasCloud) { + if (!ATLASCLOUD_API_KEY) { + throw new Error( + "Atlas Cloud selected but ATLASCLOUD_API_KEY or ATLAS_CLOUD_API_KEY is not set" + ); + } + + return { + name: "Atlas Cloud", + baseUrl: trimTrailingSlash( + Deno.env.get("ATLASCLOUD_API_BASE") || + Deno.env.get("ATLAS_CLOUD_API_BASE") || + ATLASCLOUD_BASE + ), + apiKey: ATLASCLOUD_API_KEY, + embeddingModel: + Deno.env.get("OPEN_BRAIN_EMBEDDING_MODEL") || DEFAULT_ATLASCLOUD_EMBEDDING_MODEL, + chatModel: Deno.env.get("OPEN_BRAIN_CHAT_MODEL") || DEFAULT_ATLASCLOUD_CHAT_MODEL, + }; + } + + if (!OPENROUTER_API_KEY) { + throw new Error( + "OPENROUTER_API_KEY is not set. Set it, or set OPEN_BRAIN_AI_PROVIDER=atlascloud with ATLASCLOUD_API_KEY." + ); + } + + return { + name: "OpenRouter", + baseUrl: trimTrailingSlash(Deno.env.get("OPENROUTER_BASE_URL") || OPENROUTER_BASE), + apiKey: OPENROUTER_API_KEY, + embeddingModel: + Deno.env.get("OPEN_BRAIN_EMBEDDING_MODEL") || DEFAULT_OPENROUTER_EMBEDDING_MODEL, + chatModel: Deno.env.get("OPEN_BRAIN_CHAT_MODEL") || DEFAULT_OPENROUTER_CHAT_MODEL, + }; +} + +const aiProvider = buildAiProviderConfig(); const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); type ThoughtMatch = { @@ -44,34 +109,34 @@ function thoughtUrl(id: string): string { } async function getEmbedding(text: string): Promise { - const r = await fetch(`${OPENROUTER_BASE}/embeddings`, { + const r = await fetch(`${aiProvider.baseUrl}/embeddings`, { method: "POST", headers: { - Authorization: `Bearer ${OPENROUTER_API_KEY}`, + Authorization: `Bearer ${aiProvider.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ - model: "openai/text-embedding-3-small", + model: aiProvider.embeddingModel, input: text, }), }); if (!r.ok) { const msg = await r.text().catch(() => ""); - throw new Error(`OpenRouter embeddings failed: ${r.status} ${msg}`); + throw new Error(`${aiProvider.name} embeddings failed: ${r.status} ${msg}`); } const d = await r.json(); return d.data[0].embedding; } async function extractMetadata(text: string): Promise> { - const r = await fetch(`${OPENROUTER_BASE}/chat/completions`, { + const r = await fetch(`${aiProvider.baseUrl}/chat/completions`, { method: "POST", headers: { - Authorization: `Bearer ${OPENROUTER_API_KEY}`, + Authorization: `Bearer ${aiProvider.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ - model: "openai/gpt-4o-mini", + model: aiProvider.chatModel, response_format: { type: "json_object" }, messages: [ { diff --git a/server/test-ai-provider-config.mjs b/server/test-ai-provider-config.mjs new file mode 100644 index 000000000..1b4124c5d --- /dev/null +++ b/server/test-ai-provider-config.mjs @@ -0,0 +1,23 @@ +/** + * Verifies the Open Brain MCP server exposes Atlas Cloud as an optional + * OpenAI-compatible backend without changing OpenRouter as the default path. + */ + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const source = readFileSync(new URL("./index.ts", import.meta.url), "utf8"); + +assert.match(source, /OPEN_BRAIN_AI_PROVIDER/, "provider selector env is documented in code"); +assert.match(source, /atlascloud/, "Atlas Cloud selector is supported"); +assert.match(source, /ATLASCLOUD_API_KEY/, "Atlas Cloud API key env is supported"); +assert.match(source, /ATLAS_CLOUD_API_KEY/, "Atlas Cloud alias env is supported"); +assert.match(source, /https:\/\/api\.atlascloud\.ai\/v1/, "Atlas Cloud base URL is configured"); +assert.match(source, /qwen\/qwen3\.5-flash/, "Atlas Cloud chat default model is configured"); +assert.match(source, /text-embedding-3-small/, "Atlas-compatible embedding default is configured"); +assert.match(source, /openai\/gpt-4o-mini/, "OpenRouter default chat model is preserved"); +assert.match(source, /openai\/text-embedding-3-small/, "OpenRouter default embedding model is preserved"); +assert.match(source, /\$\{aiProvider\.baseUrl\}\/embeddings/, "embeddings use selected provider base URL"); +assert.match(source, /\$\{aiProvider\.baseUrl\}\/chat\/completions/, "chat uses selected provider base URL"); + +console.log("AI provider config checks passed");