Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/01-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
>
Expand Down Expand Up @@ -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.
>
Expand Down Expand Up @@ -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).

</details>

Expand Down
5 changes: 3 additions & 2 deletions docs/open-brain-assistant-gpt-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
81 changes: 73 additions & 8 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -44,34 +109,34 @@ function thoughtUrl(id: string): string {
}

async function getEmbedding(text: string): Promise<number[]> {
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<Record<string, unknown>> {
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: [
{
Expand Down
23 changes: 23 additions & 0 deletions server/test-ai-provider-config.mjs
Original file line number Diff line number Diff line change
@@ -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");
Loading